using System;
using System.Globalization;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace INdT.Sparta.Graphics
{
    [DataContract]
    public class SpartaDrawable : SpartaObject
    {
        public enum AutoHitTestModes
        {
            Rectangle
            , Ellipse
        }

        #region Vars
        private Vector2 position = Vector2.Zero;
        private Vector2 origin = Vector2.Zero;
        private Vector2 scale = Vector2.One;
        private Color color = Color.White;
        private bool visible = true;

        private Color debugColor = Color.White;
        private bool debugDraw = false;
        #endregion

        #region Properties
        public virtual float X
        {
            get { return position.X; }
            set { position.X = value; }
        }
        public virtual float Y
        {
            get { return position.Y; }
            set { position.Y = value; }
        }
        [DataMember]
        public virtual Vector2 Position
        {
            get { return position; }
            set
            {
                X = value.X;
                Y = value.Y;
            }
        }

        [DataMember]
        public virtual int Width { get; set; }
        [DataMember]
        public virtual int Height { get; set; }

        public virtual float OriginX
        {
            get { return origin.X; }
            set { origin.X = value; }
        }
        public virtual float OriginY
        {
            get { return origin.Y; }
            set { origin.Y = value; }
        }
        [DataMember]
        public virtual Vector2 Origin
        {
            get { return origin; }
            set
            {
                OriginX = value.X;
                OriginY = value.Y;
            }
        }

        public virtual float ScaleX
        {
            get { return scale.X; }
            set { scale.X = value; }
        }
        public virtual float ScaleY
        {
            get { return scale.Y; }
            set { scale.Y = value; }
        }
        [DataMember]
        public virtual Vector2 Scale
        {
            get { return scale; }
            set
            {
                ScaleX = value.X;
                ScaleY = value.Y;
            }
        }

        [DataMember]
        public virtual float Angle { get; set; }

        [DataMember]
        public virtual Color Color
        {
            get { return color; }
            set { color = value; }
        }

        public virtual float Opacity
        {
            get
            {
                return (float)Convert.ToDouble(Color.A, CultureInfo.InvariantCulture) / 255f;
            }
            set
            {
                Color newColor = this.Color;
                newColor.A = Convert.ToByte(255f * MathHelper.Clamp(value, 0f, 1f), CultureInfo.InvariantCulture);
                this.Color = newColor;
            }
        }

        [DataMember]
        public virtual bool Visible 
        {
            get { return visible; }
            set { visible = value; }
        }

        [DataMember]
        public virtual SpriteEffects SpriteEffect { get; set; }

        [DataMember]
        public virtual float LayerDepth { get; set; }

        [DataMember]
        public virtual AutoHitTestModes AutoHitTestMode { get; set; }

        public virtual Rectangle BoundingBox
        {
            get
            {
                Rectangle boundingBox = Rectangle.Empty;
                boundingBox.X = (int)X;
                boundingBox.Y = (int)Y;
                boundingBox.Width = (int)Math.Ceiling(Width * ScaleX);
                boundingBox.Height = (int)Math.Ceiling(Height * ScaleY);
                boundingBox.Offset((int)Math.Ceiling(-OriginX * ScaleX), (int)Math.Ceiling(-OriginY * ScaleY));
                return boundingBox;
            }
        }
        
        public bool DebugDraw 
        {
            get { return debugDraw; }
            set
            {
                debugDraw = value;
                if (debugDraw)
                {
                    debugColor = new Color(SpartaGame.Random.Next(0, 127), SpartaGame.Random.Next(0, 127), SpartaGame.Random.Next(0, 127), 127);
                }
            }
        }

        #endregion

        #region Methods
        public SpartaDrawable()
        {
        }

        public virtual void LoadContent()
        {
        }

        public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            if (DebugDraw)
            {
                Texture2D debugPixel = SpartaGame.Instance.PixelTexture;
                if (debugPixel != null)
                {
                    Vector2 elementOrigin = Position;
                    Vector2 initialBBPosition = new Vector2(BoundingBox.X, BoundingBox.Y);
                    Matrix myRotationMatrix = Matrix.CreateRotationZ(Angle);

                    // Rotate relative to origin.
                    Vector2 rotatedVector = Vector2.Transform(initialBBPosition - elementOrigin, myRotationMatrix);

                    // Add origin to get final location.
                    initialBBPosition = rotatedVector + elementOrigin;

                    spriteBatch.Draw(debugPixel, initialBBPosition, new Rectangle(0, 0, BoundingBox.Width, BoundingBox.Height), debugColor, Angle, Vector2.Zero, 1f, SpriteEffects.None, LayerDepth);

                    // Origin point
                    spriteBatch.Draw(debugPixel, Position, new Rectangle(0, 0, 3, 3), Color.Black, Angle, new Vector2(1.5f), Scale, SpriteEffects.None, LayerDepth);
                }
            }
        }

        public virtual bool CollidesWithDrawable(SpartaDrawable drawable)
        {
            if (drawable == null || this == drawable)
            {
                return false;
            }
            return drawable.BoundingBox.Intersects(this.BoundingBox);
        }

        /// <summary>
        /// Generates a scalar value that can be used to compare where corners of 
        /// a rectangle have been projected onto a particular axis. 
        /// </summary>
        /// <param name="theRectangleCorner"></param>
        /// <param name="theAxis"></param>
        /// <returns></returns>
        public int GenerateScalar(Vector2 theRectangleCorner, Vector2 theAxis)
        {
            //Using the formula for Vector projection. Take the corner being passed in
            //and project it onto the given Axis
            float aNumerator = (theRectangleCorner.X * theAxis.X) + (theRectangleCorner.Y * theAxis.Y);
            float aDenominator = (theAxis.X * theAxis.X) + (theAxis.Y * theAxis.Y);
            float aDivisionResult = aNumerator / aDenominator;
            Vector2 aCornerProjected = new Vector2(aDivisionResult * theAxis.X, aDivisionResult * theAxis.Y);

            //Now that we have our projected Vector, calculate a scalar of that projection
            //that can be used to more easily do comparisons
            float aScalar = (theAxis.X * aCornerProjected.X) + (theAxis.Y * aCornerProjected.Y);
            return (int)aScalar;
        }

        /// <summary>
        /// Rotate a point from a given location and adjust using the Origin we
        /// are rotating around
        /// </summary>
        /// <param name="thePoint"></param>
        /// <param name="theOrigin"></param>
        /// <param name="theRotation"></param>
        /// <returns></returns>
        public static Vector2 RotatePoint(Vector2 thePoint, Vector2 theOrigin, float theRotation)
        {
            Vector2 aTranslatedPoint = Vector2.Zero;
            aTranslatedPoint.X = (float)(Math.Cos(theRotation) * (thePoint.X - theOrigin.X) - Math.Sin(theRotation) * (thePoint.Y - theOrigin.Y) + theOrigin.X);
            aTranslatedPoint.Y = (float)(Math.Sin(theRotation) * (thePoint.X - theOrigin.X) + Math.Cos(theRotation) * (thePoint.Y - theOrigin.Y) + theOrigin.Y);
            return aTranslatedPoint;
        }

        public Vector2 UpperLeftCorner()
        {
            Vector2 aUpperLeft = Vector2.Zero;
            aUpperLeft.X = BoundingBox.Left;
            aUpperLeft.Y = BoundingBox.Top;
            aUpperLeft = RotatePoint(aUpperLeft, Position, -Angle);
            return aUpperLeft;
        }

        public Vector2 UpperRightCorner()
        {
            Vector2 aUpperRight = Vector2.Zero;
            aUpperRight.X = BoundingBox.Right;
            aUpperRight.Y = BoundingBox.Top;
            aUpperRight = RotatePoint(aUpperRight, Position, -Angle);
            return aUpperRight;
        }

        public Vector2 LowerLeftCorner()
        {
            Vector2 aLowerLeft = Vector2.Zero;
            aLowerLeft.X = BoundingBox.Left;
            aLowerLeft.Y = BoundingBox.Bottom;
            aLowerLeft = RotatePoint(aLowerLeft, Position, -Angle);
            return aLowerLeft;
        }

        public Vector2 LowerRightCorner()
        {
            Vector2 aLowerRight = Vector2.Zero;
            aLowerRight.X = BoundingBox.Right;
            aLowerRight.Y = BoundingBox.Bottom;
            aLowerRight = RotatePoint(aLowerRight, Position, -Angle);
            return aLowerRight;
        }

        public virtual bool IsPointInObject(Vector2 point)
        {
            switch (AutoHitTestMode)
            {
                case AutoHitTestModes.Rectangle:
                    return IsPointInRectangle(point);
                case AutoHitTestModes.Ellipse:
                    return IsPointInEllipse(point);
                default:
                    return false;
            }
        }

        public bool IsPointInRectangle(Vector2 point)
        {
            Rectangle bbox;
            float width;
            float height;
            Vector2 rotatedPoint = Vector2.Zero;

            // Retrieve the bounding box
            bbox = BoundingBox;

            // If no rotation is applied, we can simply check against the bounding box
            if (Angle == 0) return bbox.Contains((int)point.X, (int)point.Y);

            // Get the sprite width and height
            width = bbox.Width;
            height = bbox.Height;

            // Subtract the sprite position to retrieve the test point in
            // object space rather than in screen space
            point -= Position;

            // Rotate the point by the negative angle of the sprite to cancel out the sprite rotation
            rotatedPoint.X = (float)(Math.Cos(-Angle) * point.X - Math.Sin(-Angle) * point.Y);
            rotatedPoint.Y = (float)(Math.Sin(-Angle) * point.X + Math.Cos(-Angle) * point.Y);

            //System.Diagnostics.Debug.WriteLine(rotatedPoint.ToString());

            // Move the bounding box to object space too
            bbox.Offset((int)-X, (int)-Y);

            // Does the bounding box contain the rotated sprite?
            return bbox.Contains((int)rotatedPoint.X, (int)rotatedPoint.Y);
        }

        protected bool IsPointInEllipse(Vector2 point)
        {
            Rectangle bbox;
            Vector2 rotatedPoint = Vector2.Zero;

            // Retrieve the basic sprite bounding box
            bbox = BoundingBox;

            // Subtract the ellipse's top-left position from the test point so that the test
            // point is relative to the origin position rather than relative to the screen
            point -= Position;

            // Rotate the point by the negative angle of the sprite to cancel out the sprite rotation
            rotatedPoint.X = (float)(Math.Cos(-Angle) * point.X - Math.Sin(-Angle) * point.Y);
            rotatedPoint.Y = (float)(Math.Sin(-Angle) * point.X + Math.Cos(-Angle) * point.Y);

            // Add back the origin point multiplied by the scale.
            // This will put us in the top-left corner of the bounding box.
            rotatedPoint += Origin * Scale;
            // Subtract the bounding box midpoint from each axis.
            // This will put us in the center of the ellipse.
            rotatedPoint -= new Vector2(bbox.Width / 2, bbox.Height / 2);

            // Divide the point by the width and height of the bounding box.
            // This will result in values between -0.5 and +0.5 on each axis for
            // positions within the bounding box. As both axes are then on the same
            // scale we can check the distance from the center point as a circle,
            // without having to worry about elliptical shapes.
            rotatedPoint /= new Vector2(bbox.Width, bbox.Height);

            //System.Diagnostics.Debug.WriteLine(rotatedPoint.Length());

            // See if the distance from the origin to the point is <= 0.5
            // (the radius of a unit-size circle). If so, we are within the ellipse.
            return (rotatedPoint.Length() <= 0.5f);
        }
        #endregion
    }
}
